--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 5d3d5114a54da393156444adf504d2bf6ff55258
Parents : d9f3e9b
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-01T20:46:18-06:00
feat(contacts): add functionality to check if a contact exists by identity hash and update UI to display contact status in call overlays
Changes
4 files changed, 62 insertions(+), 10 deletions(-)
Diff
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 7be3946d..a6b91072 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -3040,6 +3040,7 @@ class ReticulumMeshChat:
"is_speaker_muted": self.telephone_manager.telephone.receive_muted,
"is_voicemail": self.voicemail_manager.is_recording,
"call_start_time": self.telephone_manager.call_start_time,
+ "is_contact": bool(self.database.contacts.get_contact_by_identity_hash(remote_identity_hash)) if remote_identity_hash else False,
}
return web.json_response(
@@ -3150,6 +3151,7 @@ class ReticulumMeshChat:
icon = self.database.misc.get_user_icon(lxmf_hash)
if icon:
d["remote_icon"] = dict(icon)
+ d["is_contact"] = bool(self.database.contacts.get_contact_by_identity_hash(remote_identity_hash))
call_history.append(d)
return web.json_response(
@@ -3631,6 +3633,12 @@ class ReticulumMeshChat:
self.database.contacts.delete_contact(contact_id)
return web.json_response({"message": "Contact deleted"})
+ @routes.get("/api/v1/telephone/contacts/check/{identity_hash}")
+ async def telephone_contacts_check(request):
+ identity_hash = request.match_info["identity_hash"]
+ contact = self.database.contacts.get_contact_by_identity_hash(identity_hash)
+ return web.json_response({"is_contact": contact is not None, "contact": dict(contact) if contact else None})
+
# announce
@routes.get("/api/v1/announce")
async def announce_trigger(request):
diff --git a/meshchatx/src/backend/database/contacts.py b/meshchatx/src/backend/database/contacts.py
index 5c216af1..8b145ac1 100644
--- a/meshchatx/src/backend/database/contacts.py
+++ b/meshchatx/src/backend/database/contacts.py
@@ -58,3 +58,9 @@ class ContactsDAO:
def delete_contact(self, contact_id):
self.provider.execute("DELETE FROM contacts WHERE id = ?", (contact_id,))
+ def get_contact_by_identity_hash(self, remote_identity_hash):
+ return self.provider.fetchone(
+ "SELECT * FROM contacts WHERE remote_identity_hash = ?",
+ (remote_identity_hash,),
+ )
+
diff --git a/meshchatx/src/frontend/components/call/CallOverlay.vue b/meshchatx/src/frontend/components/call/CallOverlay.vue
index e62bc34c..47ebc986 100644
--- a/meshchatx/src/frontend/components/call/CallOverlay.vue
+++ b/meshchatx/src/frontend/components/call/CallOverlay.vue
@@ -69,6 +69,12 @@
<div class="font-bold text-gray-900 dark:text-white truncate px-2">
{{ activeCall.remote_identity_name || $t("call.unknown") }}
</div>
+ <div
+ v-if="activeCall.is_contact"
+ class="text-[10px] text-blue-600 dark:text-blue-400 font-medium mt-0.5"
+ >
+ In contacts
+ </div>
<div class="text-[10px] text-gray-500 dark:text-zinc-500 font-mono truncate px-4">
{{
activeCall.remote_identity_hash
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index 96e1177e..b282b718 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -100,6 +100,12 @@
}}</span>
<span v-else>{{ $t("call.unknown") }}</span>
</div>
+ <div
+ v-if="(activeCall || lastCall)?.is_contact"
+ class="text-xs text-blue-600 dark:text-blue-400 font-medium mt-1"
+ >
+ In contacts
+ </div>
<!-- identity hash -->
<div
@@ -393,16 +399,27 @@
{{ entry.remote_identity_hash }}
</div>
</div>
- <button
- type="button"
- class="text-[10px] text-blue-500 hover:text-blue-600 font-bold uppercase tracking-tighter ml-4"
- @click="
- destinationHash = entry.remote_identity_hash;
- call(destinationHash);
- "
- >
- Call Back
- </button>
+ <div class="flex items-center gap-1">
+ <button
+ v-if="!entry.is_contact"
+ type="button"
+ class="p-1.5 text-gray-400 hover:text-blue-500 transition-colors"
+ :title="'Add to contacts'"
+ @click="addContactFromHistory(entry)"
+ >
+ <MaterialDesignIcon icon-name="account-plus" class="size-4" />
+ </button>
+ <button
+ type="button"
+ class="text-[10px] text-blue-500 hover:text-blue-600 font-bold uppercase tracking-tighter"
+ @click="
+ destinationHash = entry.remote_identity_hash;
+ call(destinationHash);
+ "
+ >
+ Call Back
+ </button>
+ </div>
</div>
</div>
</div>
@@ -1212,6 +1229,21 @@ export default {
console.log(e);
}
},
+ async addContactFromHistory(entry) {
+ const name = prompt("Enter contact name:", entry.remote_identity_name || "");
+ if (!name) return;
+ try {
+ await window.axios.post("/api/v1/telephone/contacts", {
+ name: name,
+ remote_identity_hash: entry.remote_identity_hash,
+ });
+ ToastUtils.success("Contact added");
+ this.getHistory();
+ this.getContacts();
+ } catch (e) {
+ ToastUtils.error(e.response?.data?.message || "Failed to add contact");
+ }
+ },
async getHistory() {
try {
const response = await window.axios.get("/api/v1/telephone/history?limit=10");
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────